import { createMcpHandler } from 'mcp-handler';
import { z } from 'zod';
import axios from 'axios';
const NEIS_API_KEY = process.env.NEIS_API_KEY || '';
const NEIS_BASE_URL = 'https://open.neis.go.kr/hub';
// Helper function to format date as YYYYMMDD
function formatDate(date?: string): string {
if (date) return date;
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return `${year}${month}${day}`;
}
// Helper function to make NEIS API calls
async function callNeisApi(endpoint: string, params: Record<string, any>) {
try {
const response = await axios.get(`${NEIS_BASE_URL}/${endpoint}`, {
params: {
KEY: NEIS_API_KEY,
Type: 'json',
pIndex: 1,
pSize: 100,
...params,
},
});
return response.data;
} catch (error) {
throw new Error(`NEIS API error: ${error}`);
}
}
const handler = createMcpHandler((server) => {
// Register tool: search school
server.tool(
'search school',
'Search for Korean schools by name. Returns school code, office code, and full school name needed for other NEIS queries.',
{
name: z.string().describe('School name to search for (Korean or partial name)'),
},
async ({ name }) => {
const data = await callNeisApi('schoolInfo', {
SCHUL_NM: name,
});
const schools = data.schoolInfo?.[1]?.row || [];
if (schools.length === 0) {
return {
content: [
{
type: 'text',
text: `No schools found matching: ${name}`,
},
],
};
}
const results = schools.map((school: any) => ({
schoolCode: school.SD_SCHUL_CODE,
officeCode: school.ATPT_OFCDC_SC_CODE,
schoolName: school.SCHUL_NM,
address: school.ORG_RDNMA,
type: school.SCHUL_KND_SC_NM,
}));
return {
content: [
{
type: 'text',
text: JSON.stringify(results, null, 2),
},
],
};
}
);
// Register tool: get meal
server.tool(
'get meal',
'Get school meal information (breakfast, lunch, dinner) for a specific school and date.',
{
schoolCode: z.string().describe('School code (SD_SCHUL_CODE from school search)'),
officeCode: z.string().describe('Office of education code (ATPT_OFCDC_SC_CODE from school search)'),
ymd: z.string().optional().describe('Date in YYYYMMDD format (defaults to today)'),
},
async ({ schoolCode, officeCode, ymd }) => {
const date = formatDate(ymd);
const data = await callNeisApi('mealServiceDietInfo', {
ATPT_OFCDC_SC_CODE: officeCode,
SD_SCHUL_CODE: schoolCode,
MLSV_YMD: date,
});
const meals = data.mealServiceDietInfo?.[1]?.row || [];
if (meals.length === 0) {
return {
content: [
{
type: 'text',
text: `No meal information found for date: ${date}`,
},
],
};
}
const results = meals.map((meal: any) => ({
date: meal.MLSV_YMD,
mealType: meal.MMEAL_SC_NM,
menu: meal.DDISH_NM.replace(/<br\/>/g, '\n'),
calories: meal.CAL_INFO,
nutrition: meal.NTR_INFO,
}));
return {
content: [
{
type: 'text',
text: JSON.stringify({ date, meals: results }, null, 2),
},
],
};
}
);
// Register tool: get timetable
server.tool(
'get timetable',
'Get class timetable for a specific school, grade, and class.',
{
schoolCode: z.string().describe('School code (SD_SCHUL_CODE from school search)'),
grade: z.string().optional().describe('Grade level (1-6 for elementary, 1-3 for middle/high)'),
className: z.string().optional().describe('Class name/number'),
ymd: z.string().optional().describe('Date in YYYYMMDD format (defaults to today)'),
},
async ({ schoolCode, grade, className, ymd }) => {
const date = formatDate(ymd);
// Determine school type and use appropriate endpoint
// For simplicity, we'll try high school first
const data = await callNeisApi('hisTimetable', {
SD_SCHUL_CODE: schoolCode,
ALL_TI_YMD: date,
GRADE: grade,
CLASS_NM: className,
});
const classes = data.hisTimetable?.[1]?.row || [];
if (classes.length === 0) {
return {
content: [
{
type: 'text',
text: `No timetable found for date: ${date}`,
},
],
};
}
const results = classes.map((cls: any) => ({
period: cls.PERIO,
subject: cls.ITRT_CNTNT,
teacher: cls.CLRM_NM,
grade: cls.GRADE,
class: cls.CLASS_NM,
}));
return {
content: [
{
type: 'text',
text: JSON.stringify({ date, timetable: results }, null, 2),
},
],
};
}
);
// Register tool: get schedule
server.tool(
'get schedule',
'Get school schedule and events for a specific date or date range.',
{
schoolCode: z.string().describe('School code (SD_SCHUL_CODE from school search)'),
officeCode: z.string().optional().describe('Office of education code (ATPT_OFCDC_SC_CODE from school search)'),
ymd: z.string().optional().describe('Date in YYYYMMDD format (defaults to today)'),
},
async ({ schoolCode, officeCode, ymd }) => {
const date = formatDate(ymd);
const data = await callNeisApi('SchoolSchedule', {
SD_SCHUL_CODE: schoolCode,
ATPT_OFCDC_SC_CODE: officeCode,
AA_YMD: date,
});
const events = data.SchoolSchedule?.[1]?.row || [];
if (events.length === 0) {
return {
content: [
{
type: 'text',
text: `No events found for date: ${date}`,
},
],
};
}
const results = events.map((event: any) => ({
date: event.AA_YMD,
eventName: event.EVENT_NM,
eventDetail: event.EVENT_CNTNT,
grade: event.SBTR_DD_SC_NM,
}));
return {
content: [
{
type: 'text',
text: JSON.stringify({ date, events: results }, null, 2),
},
],
};
}
);
});
export { handler as GET, handler as POST };